module ActsAsCsv
  # `included` is called wheneven the module gets included
  # in any other module. Classes are modules as
  # well. `included` gets as an argument the class, in which
  # the `include` call is made.
  def self.included(base)
    # Q: Where does extend come from? A: Probably a module
    # thing.
    base.extend ClassMethods
  end

  # Q: Why have this ClassMethods module, only to include
  # InstanceMethods? A: Maybe to make the example more
  # generally applicable.

  # Q: Why do class methods include instance methods? A: ???

  # Q: Where is the distinction in the code, what makes an
  # instance method and what makes a class method? A: ???
  module ClassMethods
    def acts_as_csv
      include InstanceMethods
    end
  end

  # InstanceMethods contains the methods, which will be
  # added to any class, for which acts_as_csv is called.
  module InstanceMethods
    def read
      @csv_contents = []
      # Q: Why '.txt' and not '.csv'? A: Maybe to make it
      # clean, that this is a mere plaintext file?
      filename = self.class.to_s.downcase + '.txt'
      file = File.new(filename)
      # Get one line from the file and split by ', '.
      @headers = file.gets.chomp.split(', ')

      # For each line of the file split the line and append
      # it to the contents field.
      file.each do |row|
        @csv_contents << row.chomp.split(', ')
      end
    end

    # Task: Add an each method to yield rows to a block.
    def each
      @csv_contents.each do |values|
        yield CsvRow.new(self, values)
      end
    end

    # Add accessors for @headers and @csv_contents.
    attr_accessor :headers, :csv_contents

    # Initializing will read the file using the instance
    # method `read`.
    def initialize
      read
    end
  end
end

# Task: Use method_missing to return the value of the row
# for a given heading.
class CsvRow
  # Store the parent as a reference, so that we can lookup
  # headers later. Otherwise the CsvRow has to store the
  # headers, duplicating that information.
  def initialize(parent, values)
    @parent = parent
    @values = values
  end

  # `method_missing` is called, when an object receives a
  # message, for which it has no method. This can be used to
  # dynamically act upon these messages, depending on their
  # name and the arguments.
  def method_missing(method_name, *args, &block)
    ind_of_col = @parent.headers.find_index(method_name.to_s)
    if (not ind_of_col.nil?)
      return @values[ind_of_col]
    end
    raise NoMethodError, "message not understood: #{method_name}, arguments #{args}"
  end
end


class RubyCsv
  include ActsAsCsv
  acts_as_csv
end


# Initialize the object, will call read.
csv = RubyCsv.new
# Output fields.
puts csv.headers.inspect
puts csv.csv_contents.inspect

csv.each do |row|
  puts "A: #{row.A}"
end
